/*
 * Created on 2 Oct 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.networkmanager.impl.http;

import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.peer.impl.PEPeerTransport;
import org.gudy.azureus2.core3.torrent.TOTorrentFile;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Debug;

import com.aelitis.azureus.core.networkmanager.NetworkConnection;
import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.networkmanager.Transport;
import com.aelitis.azureus.core.networkmanager.impl.TransportHelper;
import com.aelitis.azureus.core.networkmanager.impl.tcp.IncomingSocketChannelManager;
import com.aelitis.azureus.core.peermanager.PeerManager;
import com.aelitis.azureus.core.peermanager.PeerManagerRegistration;
import com.aelitis.azureus.core.peermanager.PeerManagerRoutingListener;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamDecoder;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamEncoder;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamFactory;
import com.aelitis.azureus.core.stats.AzureusCoreStats;
import com.aelitis.azureus.core.stats.AzureusCoreStatsProvider;
import com.aelitis.azureus.core.util.CopyOnWriteList;

public class HTTPNetworkManager {
    private static final String NL = "\r\n";

    private static final LogIDs LOGID = LogIDs.NWMAN;

    private static final HTTPNetworkManager instance = new HTTPNetworkManager();

    public static HTTPNetworkManager getSingleton() {
        return (instance);
    }

    private final IncomingSocketChannelManager http_incoming_manager;

    private long total_requests;
    private long total_webseed_requests;
    private long total_getright_requests;
    private long total_invalid_requests;
    private long total_ok_requests;

    private CopyOnWriteList<URLHandler> url_handlers = new CopyOnWriteList<URLHandler>();

    private HTTPNetworkManager() {
        Set types = new HashSet();

        types.add(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_COUNT);
        types.add(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_OK_COUNT);
        types.add(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_INVALID_COUNT);
        types.add(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT);
        types.add(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT);

        AzureusCoreStats.registerProvider(types, new AzureusCoreStatsProvider() {
            public void updateStats(Set types, Map values) {
                if (types.contains(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_COUNT)) {

                    values.put(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_COUNT, new Long(total_requests));
                }

                if (types.contains(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_OK_COUNT)) {

                    values.put(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_OK_COUNT, new Long(total_ok_requests));
                }

                if (types.contains(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_INVALID_COUNT)) {

                    values.put(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_INVALID_COUNT, new Long(total_invalid_requests));
                }

                if (types.contains(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT)) {

                    values.put(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT, new Long(total_webseed_requests));
                }

                if (types.contains(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT)) {

                    values.put(AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT, new Long(total_getright_requests));
                }

            }
        });
        /*
         * try{ System.out.println( "/webseed?info_hash=" + URLEncoder.encode( new String(
         * ByteFormatter.decodeString("C9C04D96F11FB5C5ECC99D418D3575FBFC2208B0"), "ISO-8859-1"), "ISO-8859-1" )); }catch( Throwable e ){
         * e.printStackTrace(); }
         */

        http_incoming_manager = new IncomingSocketChannelManager("HTTP.Data.Listen.Port", "HTTP.Data.Listen.Port.Enable");

        NetworkManager.ByteMatcher matcher = new NetworkManager.ByteMatcher() {
            public int matchThisSizeOrBigger() {
                return (4 + 1 + 11);
            } // GET ' ' <url of 1> ' HTTP/1.1<cr><nl>'

            public int maxSize() {
                return 256;
            } // max GET <url> size - boiler plate plus small url plus hash

            public int minSize() {
                return 3;
            } // enough to match GET

            public Object matches(TransportHelper transport, ByteBuffer to_compare, int port) {
                total_requests++;

                InetSocketAddress address = transport.getAddress();

                int old_limit = to_compare.limit();
                int old_position = to_compare.position();

                boolean ok = false;

                try {
                    byte[] head = new byte[3];

                    to_compare.get(head);

                    // note duplication of this in min-matches below

                    if (head[0] != 'G' || head[1] != 'E' || head[2] != 'T') {

                        return (null);
                    }

                    byte[] line_bytes = new byte[to_compare.remaining()];

                    to_compare.get(line_bytes);

                    try {
                        // format is GET url HTTP/1.1<NL>

                        String url = new String(line_bytes, "ISO-8859-1");

                        int space = url.indexOf(' ');

                        if (space == -1) {

                            return (null);
                        }

                        // note that we don't insist on a full URL here, just the start of one

                        url = url.substring(space + 1).trim();

                        if (url.indexOf("/index.html") != -1) {

                            ok = true;

                            return (new Object[] { transport, getIndexPage() });

                        } else if (url.indexOf("/ping.html") != -1) {

                            // ping is used for inbound HTTP port checking

                            ok = true;

                            return (new Object[] { transport, getPingPage(url) });

                        } else if (url.indexOf("/test503.html") != -1) {

                            ok = true;

                            return (new Object[] { transport, getTest503() });
                        }

                        String hash_str = null;

                        int hash_pos = url.indexOf("?info_hash=");

                        if (hash_pos != -1) {

                            int hash_start = hash_pos + 11;

                            int hash_end = url.indexOf('&', hash_pos);

                            if (hash_end == -1) {

                                // not read the end yet

                                return (null);

                            } else {

                                hash_str = url.substring(hash_start, hash_end);
                            }
                        } else {

                            hash_pos = url.indexOf("/files/");

                            if (hash_pos != -1) {

                                int hash_start = hash_pos + 7;

                                int hash_end = url.indexOf('/', hash_start);

                                if (hash_end == -1) {

                                    // not read the end of the hash yet

                                    return (null);

                                } else {

                                    hash_str = url.substring(hash_start, hash_end);
                                }
                            }
                        }

                        if (hash_str != null) {

                            byte[] hash = URLDecoder.decode(hash_str, "ISO-8859-1").getBytes("ISO-8859-1");

                            PeerManagerRegistration reg_data = PeerManager.getSingleton().manualMatchHash(address, hash);

                            if (reg_data != null) {

                                // trim back URL as it currently has header in it too

                                int pos = url.indexOf(' ');

                                String trimmed = pos == -1 ? url : url.substring(0, pos);

                                ok = true;

                                return (new Object[] { trimmed, reg_data });
                            }
                        } else {

                            int link_pos = url.indexOf("/links/");

                            if (link_pos != -1) {

                                int pos = url.indexOf(' ', link_pos);

                                if (pos == -1) {

                                    return (null);
                                }

                                String link = url.substring(0, pos).substring(link_pos + 7);

                                link = URLDecoder.decode(link, "UTF-8");

                                PeerManagerRegistration reg_data = PeerManager.getSingleton().manualMatchLink(address, link);

                                if (reg_data != null) {

                                    TOTorrentFile file = reg_data.getLink(link);

                                    if (file != null) {

                                        StringBuffer target_url = new StringBuffer(512);

                                        target_url.append("/files/");

                                        target_url.append(URLEncoder.encode(new String(file.getTorrent().getHash(), "ISO-8859-1"), "ISO-8859-1"));

                                        byte[][] bits = file.getPathComponents();

                                        for (int i = 0; i < bits.length; i++) {

                                            target_url.append("/");

                                            target_url.append(URLEncoder.encode(new String(bits[i], "ISO-8859-1"), "ISO-8859-1"));
                                        }

                                        ok = true;

                                        return (new Object[] { target_url.toString(), reg_data });
                                    }
                                }
                            }
                        }

                        String trimmed = url;

                        int pos = trimmed.indexOf(' ');

                        if (pos != -1) {

                            trimmed = trimmed.substring(0, pos);
                        }

                        for (URLHandler handler : url_handlers) {

                            if (handler.matches(trimmed)) {

                                ok = true;

                                return (new Object[] { handler, transport, "GET " + url });
                            }
                        }
                        if (Logger.isEnabled()) {
                            Logger.log(new LogEvent(LOGID, "HTTP decode from " + address + " failed: no match for " + url));
                        }

                        return (new Object[] { transport, getNotFound() });

                    } catch (Throwable e) {

                        if (Logger.isEnabled()) {
                            Logger.log(new LogEvent(LOGID, "HTTP decode from " + address + " failed, " + e.getMessage()));
                        }

                        return (null);
                    }
                } finally {

                    if (ok) {

                        total_ok_requests++;

                    } else {

                        total_invalid_requests++;
                    }
                    // restore buffer structure

                    to_compare.limit(old_limit);
                    to_compare.position(old_position);
                }
            }

            public Object minMatches(TransportHelper transport, ByteBuffer to_compare, int port) {
                byte[] head = new byte[3];

                to_compare.get(head);

                if (head[0] != 'G' || head[1] != 'E' || head[2] != 'T') {

                    return (null);
                }

                return ("");
            }

            public byte[][] getSharedSecrets() {
                return (null);
            }

            public int getSpecificPort() {
                return (http_incoming_manager.getTCPListeningPortNumber());
            }
        };

        // register for incoming connection routing
        NetworkManager.getSingleton().requestIncomingConnectionRouting(matcher, new NetworkManager.RoutingListener() {
            public void connectionRouted(final NetworkConnection connection, Object _routing_data) {
                Object[] x = (Object[]) _routing_data;

                Object entry1 = x[0];

                if (entry1 instanceof TransportHelper) {

                    // routed on failure

                    writeReply(connection, (TransportHelper) x[0], (String) x[1]);

                    return;

                } else if (entry1 instanceof URLHandler) {

                    ((URLHandler) entry1).handle((TransportHelper) x[1], (String) x[2]);

                    return;
                }

                final String url = (String) entry1;
                final PeerManagerRegistration routing_data = (PeerManagerRegistration) x[1];

                if (Logger.isEnabled()) {
                    Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress()
                            + " routed successfully on '" + url + "'"));
                }

                PeerManager.getSingleton().manualRoute(routing_data, connection, new PeerManagerRoutingListener() {
                    public boolean routed(PEPeerTransport peer) {
                        if (url.indexOf("/webseed") != -1) {

                            total_webseed_requests++;

                            new HTTPNetworkConnectionWebSeed(HTTPNetworkManager.this, connection, peer);

                            return (true);

                        } else if (url.indexOf("/files/") != -1) {

                            total_getright_requests++;

                            new HTTPNetworkConnectionFile(HTTPNetworkManager.this, connection, peer);

                            return (true);
                        }

                        return (false);
                    }
                });
            }

            public boolean autoCryptoFallback() {
                return (false);
            }
        }, new MessageStreamFactory() {
            public MessageStreamEncoder createEncoder() {
                return new HTTPMessageEncoder();
            }

            public MessageStreamDecoder createDecoder() {
                return new HTTPMessageDecoder();
            }
        });
    }

    protected void reRoute(final HTTPNetworkConnection old_http_connection, final byte[] old_hash, final byte[] new_hash, final String header) {
        final NetworkConnection old_connection = old_http_connection.getConnection();

        PeerManagerRegistration reg_data = PeerManager.getSingleton().manualMatchHash(old_connection.getEndpoint().getNotionalAddress(), new_hash);

        if (reg_data == null) {

            old_http_connection.close("Re-routing failed - registration not found");

            return;
        }

        final Transport transport = old_connection.detachTransport();

        old_http_connection.close("Switching torrents");

        final NetworkConnection new_connection =
                NetworkManager.getSingleton().bindTransport(transport, new HTTPMessageEncoder(), new HTTPMessageDecoder(header));

        PeerManager.getSingleton().manualRoute(reg_data, new_connection, new PeerManagerRoutingListener() {
            public boolean routed(PEPeerTransport peer) {
                HTTPNetworkConnection new_http_connection;

                if (header.indexOf("/webseed") != -1) {

                    new_http_connection = new HTTPNetworkConnectionWebSeed(HTTPNetworkManager.this, new_connection, peer);

                } else if (header.indexOf("/files/") != -1) {

                    new_http_connection = new HTTPNetworkConnectionFile(HTTPNetworkManager.this, new_connection, peer);

                } else {

                    return (false);
                }

                // fake a wakeup so pre-read header is processed

                new_http_connection.readWakeup();

                /*
                 * System.out.println( "Re-routed " + new_connection.getEndpoint().getNotionalAddress() + " from " + ByteFormatter.encodeString(
                 * old_hash ) + " to " + ByteFormatter.encodeString( new_hash ) );
                 */

                return (true);
            }
        });
    }

    public boolean isHTTPListenerEnabled() {
        return (http_incoming_manager.isEnabled());
    }

    public int getHTTPListeningPortNumber() {
        return (http_incoming_manager.getTCPListeningPortNumber());
    }

    public void setExplicitBindAddress(InetAddress address) {
        http_incoming_manager.setExplicitBindAddress(address);
    }

    public void clearExplicitBindAddress() {
        http_incoming_manager.clearExplicitBindAddress();
    }

    public boolean isEffectiveBindAddress(InetAddress address) {
        return (http_incoming_manager.isEffectiveBindAddress(address));
    }

    protected String getIndexPage() {
        return ("HTTP/1.1 200 OK" + NL + "Connection: Close" + NL + "Content-Length: 0" + NL + NL);
    }

    protected String getPingPage(String url) {
        int pos = url.indexOf(' ');

        if (pos != -1) {

            url = url.substring(0, pos);
        }

        pos = url.indexOf('?');

        Map response = new HashMap();

        boolean ok = false;

        if (pos != -1) {

            StringTokenizer tok = new StringTokenizer(url.substring(pos + 1), "&");

            while (tok.hasMoreTokens()) {

                String token = tok.nextToken();

                pos = token.indexOf('=');

                if (pos != -1) {

                    String lhs = token.substring(0, pos);
                    String rhs = token.substring(pos + 1);

                    if (lhs.equals("check")) {

                        response.put("check", rhs);

                        ok = true;
                    }
                }
            }
        }

        if (ok) {

            try {
                byte[] bytes = BEncoder.encode(response);

                byte[] length = new byte[4];

                ByteBuffer.wrap(length).putInt(bytes.length);

                return ("HTTP/1.1 200 OK" + NL + "Connection: Close" + NL + "Content-Length: " + (bytes.length + 4) + NL + NL
                        + new String(length, "ISO-8859-1") + new String(bytes, "ISO-8859-1"));

            } catch (Throwable e) {
            }
        }

        return (getNotFound());

    }

    protected String getTest503() {
        return ("HTTP/1.1 503 Service Unavailable" + NL + "Connection: Close" + NL + "Content-Length: 4" + NL + NL + "1234");
    }

    protected String getNotFound() {
        return ("HTTP/1.1 404 Not Found" + NL + "Connection: Close" + NL + "Content-Length: 0" + NL + NL);
    }

    protected String getRangeNotSatisfiable() {
        return ("HTTP/1.1 416 Not Satisfiable" + NL + "Connection: Close" + NL + "Content-Length: 0" + NL + NL);
    }

    protected void writeReply(final NetworkConnection connection, final TransportHelper transport, final String data) {
        byte[] bytes;

        try {
            bytes = data.getBytes("ISO-8859-1");

        } catch (UnsupportedEncodingException e) {

            bytes = data.getBytes();
        }

        final ByteBuffer bb = ByteBuffer.wrap(bytes);

        try {
            transport.write(bb, false);

            if (bb.remaining() > 0) {

                transport.registerForWriteSelects(new TransportHelper.selectListener() {
                    public boolean selectSuccess(TransportHelper helper, Object attachment) {
                        try {
                            int written = helper.write(bb, false);

                            if (bb.remaining() > 0) {

                                helper.registerForWriteSelects(this, null);

                            } else {

                                if (Logger.isEnabled()) {
                                    Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress()
                                            + " closed"));
                                }

                                connection.close(null);
                            }

                            return (written > 0);

                        } catch (Throwable e) {

                            helper.cancelWriteSelects();

                            if (Logger.isEnabled()) {
                                Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress()
                                        + " failed to write error '" + data + "'"));
                            }

                            connection.close(e == null ? null : Debug.getNestedExceptionMessage(e));

                            return (false);
                        }
                    }

                    public void selectFailure(TransportHelper helper, Object attachment, Throwable msg) {
                        helper.cancelWriteSelects();

                        if (Logger.isEnabled()) {
                            Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress()
                                    + " failed to write error '" + data + "'"));
                        }

                        connection.close(msg == null ? null : Debug.getNestedExceptionMessage(msg));
                    }
                }, null);
            } else {

                if (Logger.isEnabled()) {
                    Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " closed"));
                }

                connection.close(null);
            }
        } catch (Throwable e) {

            if (Logger.isEnabled()) {
                Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " failed to write error '"
                        + data + "'"));
            }

            connection.close(e == null ? null : Debug.getNestedExceptionMessage(e));
        }
    }

    public void addURLHandler(URLHandler handler) {
        url_handlers.add(handler);
    }

    public void removeURLHandler(URLHandler handler) {
        url_handlers.remove(handler);
    }

    public interface URLHandler {
        public boolean matches(String url);

        public void handle(TransportHelper transport, String header_so_far);

    }
}
